/**
 * Licensed to Open-Ones Group under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Open-Ones Group licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a
 * copy of the License at:
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package com.hoangqp.android.stardictcore;

import java.io.BufferedInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.List;

import android.content.Context;
import android.util.Log;

import com.hoangqp.util.InternalStorage;
import com.hoangqp.lookdict.TOPActivity;
import com.hoangqp.stardict.parse.FileFormatErrorException;

public class IdxFile {

	private static final int BYTE_ARRAY_SIZE = 256;

	static final int SIZE = 4 * 1024;
	static byte[] buffer = new byte[SIZE];

	private int readlimit = 8192;

	private static int END_POSITION = 0;

	// private static final int MORE_ITEMS = 1000000;

	/** constant of 0x000000FF */
	private final int byteFirst = 0x000000FF;

	/** constant of 0x0000FF00 */
	private final int secondByte = 0x0000FF00;

	/** constant of 0x00FF0000 */
	private final int thirdByte = 0x00FF0000;

	/** constant of 0xFF000000 */
	private final int fourthByte = 0xFF000000;

	/** constant of 0xFFFFFFFFL */
	private final long fixthByte = 0xFFFFFFFFL;

	/** constant of number3 */
	private final int noThree = 3;

	/** constant of number 8 */
	private final int noEight = 8;

	/** constant of number 9 */
	private final int noNine = 9;

	/** constant of number 16 */
	private final int noSixteen = 16;

	/** constant of number 24 */
	private final int noTwentyFour = 24;

	/** constant of number 4 */
	private final int aByte = 4;

	/** path to ".idx" file */
	private String strFileName;

	/** decide if properties are loaded */
	private boolean boolIsLoaded = false;

	/** number of word */
	private long longWordCount;

	/** File size. */
	private long longIdxFileSize;

	/** store the list of entries */
	private List<WordEntry> entryList;

	private FileInputStream fileInputStream = null;
	
	private File path = null;
	
	private Context context;
	
	private InternalStorage internaltorage = null;

	/**
	 * constructor.
	 * 
	 * @param fileName
	 *            path to .idx file.
	 * @param wordCount
	 *            number of word.
	 * @param fileSize
	 *            the file size.
	 * @throws FileNotFoundException
	 */
	public IdxFile(String fileName, long wordCount, long fileSize)
			throws FileNotFoundException {
		longWordCount = wordCount;
		longIdxFileSize = fileSize;
		strFileName = fileName;
		path = new File(strFileName);
		fileInputStream = new FileInputStream(strFileName);
		load();
	}
	
	public IdxFile(String fileName, long wordCount, long fileSize, Context context)
			throws FileNotFoundException {
		longWordCount = wordCount;
		longIdxFileSize = fileSize;
		strFileName = fileName;
		path = new File(strFileName);
		fileInputStream = new FileInputStream(strFileName);
		this.context = context;
		load();
	}

	/**
	 * accessor of longIdxFileSize.
	 * 
	 * @return longIdxFileSize
	 */
	public long getLongIdxFileSize() {
		return longIdxFileSize;
	}

	/**
	 * accessor of boolIsLoaded.
	 * 
	 * @return boolIsLoaded
	 */
	public boolean isLoaded() {
		return boolIsLoaded;
	}

	/**
	 * accessor of longWordCount.
	 * 
	 * @return longWordCount
	 */
	public long getLongWordCount() {
		return longWordCount;
	}

	/**
	 * accessor of entryList.
	 * 
	 * @return entryList
	 */
	public List<WordEntry> getEntryList() {
		return entryList;
	}

	/**
	 * load more item
	 * 
	 * @param currentItem
	 * @return
	 * @author Hoang
	 * @throws FileTooBigException
	 */
	public List<WordEntry> getListLoadMore() throws FileTooBigException {

		//List<WordEntry> words = new ArrayList<WordEntry>();

		File file = new File(strFileName);
		if (file.length() > longIdxFileSize) {
			throw new FileTooBigException();
		}

		BufferedInputStream bufferedInputStream = null;
		FileChannel chanel = null;

		entryList.clear();
		
		try {
			bufferedInputStream = new BufferedInputStream(fileInputStream);

			chanel = fileInputStream.getChannel();
			// MappedByteBuffer out = chanel.map(FileChannel.MapMode.READ_ONLY,
			// 0, chanel.size());
			// the buffer now reads the file as if it were loaded in memory.
			// note
			// that for smaller files it would be faster
			// to just load the file in memory
			// lets see if this buffer is loaded fully into memory
			// System.out.println(out.isLoaded());
			// the mappedbytebuffer can be used as a normal buffer to do read
			// and/or
			// write operations
			// read the size
			// System.out.println(out.capacity());
			
			long skipBytes = END_POSITION;
			bufferedInputStream.skip(skipBytes);

//			MappedByteBuffer out = chanel.map(FileChannel.MapMode.READ_WRITE,
//					END_POSITION, chanel.size());
//			chanel.truncate(out.position());
			// byte[] bt = new byte[(int) longIdxFileSize];
			// bufferedInputStream.read(bt);

			WordEntry tmpEntry = null;

			// int startPos; // start position of entry
			// int endPos = END_POSITION; // end position of entry

			String word;
			int count = 0;

			while ((word = readString(bufferedInputStream)) != null) {
				count++;
				if (count > 100000)
					break;
				tmpEntry = new WordEntry();
				tmpEntry.setStrWord(word);
				tmpEntry.setStrLwrWord(tmpEntry.getStrWord().toLowerCase());
				tmpEntry.setLongOffset(readInt(bufferedInputStream));

				tmpEntry.setLongSize(readInt(bufferedInputStream));

				entryList.add(tmpEntry);
			}


			//
			// for (long i = 0; i < longWordCount; i++) {
			// count++;
			// if (count > 100000)
			// break;
			//
			// tmpEntry = new WordEntry();
			// // read the word
			// startPos = endPos;
			//
			// while (bt[endPos] != '\0') {
			// endPos++;
			// }
			// tmpEntry.setStrWord(new String(bt, startPos, endPos - startPos,
			// "UTF8"));
			// tmpEntry.setStrLwrWord(tmpEntry.getStrWord().toLowerCase());
			//
			// // read the offset of the meaning
			// ++endPos;
			// tmpEntry.setLongOffset(readAnInt32(bt, endPos));
			//
			// // read the size of the meaning
			// endPos += aByte;
			// tmpEntry.setLongSize(readAnInt32(bt, endPos));
			// endPos += aByte;
			//
			// words.add(tmpEntry);
			// }

			// END_POSITION = endPos;
			boolIsLoaded = true;
		} catch (Exception ex) {
			System.out.println("Error: " + ex);
		}
		return entryList;
	}

	/**
	 * accessor of strFileName.
	 * 
	 * @return strFileName
	 */
	public String getStrFileName() {
		return strFileName;
	}

	/**
	 * load properties.
	 */
	public void load() {
		if (boolIsLoaded || (!(new java.io.File(strFileName)).exists())) {
			return;
		}

		BufferedInputStream bufferedInputStream = null;
		FileChannel chanel = null;
		
		try {

			bufferedInputStream = new BufferedInputStream(fileInputStream);

			chanel = fileInputStream.getChannel();

			entryList = new ArrayList<WordEntry>();
			//int startPos; // start position of entry
			//int endPos = 0; // end position of entry
			WordEntry tmpEntry = null;

			String word;

			int count = 0;

			while ((word = readString(bufferedInputStream)) != null) {

				tmpEntry = new WordEntry();
				tmpEntry.setStrWord(word);
				tmpEntry.setStrLwrWord(tmpEntry.getStrWord().toLowerCase());
				tmpEntry.setLongOffset(readInt(bufferedInputStream));

				tmpEntry.setLongSize(readInt(bufferedInputStream));
				entryList.add(tmpEntry);
				
//				count++;
//				if(count <= 100000) {
//					entryList.add(tmpEntry);
//				}
//				else {
//					//break;
//					try {
//						InternalStorage.writeObject(context, path.getName(), tmpEntry);
//					} catch(Exception e){
//						e.printStackTrace();
//					} finally {
//						Log.e("INTERNAL", "finally catch, " + count);
//					}
//				}
			}

			// for (long i = 0; i < longWordCount; i++) {
			// count++;
			// if (count > 50000)
			// break;
			// tmpEntry = new WordEntry();
			// // read the word
			// startPos = endPos;
			//
			// while (bt[endPos] != '\0') {
			// endPos++;
			// }
			// tmpEntry.setStrWord(new String(bt, startPos, endPos - startPos,
			// "UTF8"));
			// tmpEntry.setStrLwrWord(tmpEntry.getStrWord().toLowerCase());
			//
			// // read the offset of the meaning
			// ++endPos;
			// tmpEntry.setLongOffset(readAnInt32(bt, endPos));
			//
			// // read the size of the meaning
			// endPos += aByte;
			// tmpEntry.setLongSize(readAnInt32(bt, endPos));
			// endPos += aByte;
			// entryList.add(tmpEntry);
			//
			// END_POSITION = endPos;
			// }

			boolIsLoaded = true;
		} catch (Exception ex) {
			System.out.println("Error: " + ex);
		}
	}

	/**
	 * reload .idx file.
	 */
	public void reload() {
		boolIsLoaded = false;
		// load();
	}

	public void loadDataInStream(FileInputStream f)
			throws FileNotFoundException, IOException {
		FileChannel ch = f.getChannel();
		long red = 0L;
		int nRead, nGet;
		
		do {
			long read = Math.min(Integer.MAX_VALUE, ch.size() - red);
			MappedByteBuffer mb = ch.map(FileChannel.MapMode.READ_ONLY, red,
					read);

			while (mb.hasRemaining()) {
				nGet = Math.min(mb.remaining(), SIZE);
				mb.get(buffer, 0, nGet);
				//System.out.println(new String(buffer, "UTF8"));
				for (int i = 0; i < nGet; i++) {
					// p.check(buffer[i]);
					System.out.println(buffer[i]);
				}
			}
			red += read;
		} while (red < ch.size());
	}

	public void loadData(FileInputStream f) throws IOException{
		FileChannel ch = f.getChannel( );
		ByteBuffer bb = ByteBuffer.allocateDirect(SIZE);
		long checkSum = 0L;
		int nRead;
		while ( (nRead=ch.read( bb )) != -1 )
		{
		    bb.position(0);
		    bb.limit(nRead);
		    while ( bb.hasRemaining() )
		        checkSum += bb.get();
		    bb.clear( );
		}
	}
	/**
	 * convert 4 char array to an integer.
	 * 
	 * @param str
	 *            array of byte that is read from .idx file.
	 * @param beginPos
	 *            the position of a word.
	 * @return a long.
	 */
	private long readAnInt32(byte[] str, int beginPos) {
		int firstByte = (byteFirst & ((int) str[beginPos]));
		int secondByte = (byteFirst & ((int) str[beginPos + 1]));
		int thirdByte = (byteFirst & ((int) str[beginPos + 2]));
		int fourthByte = (byteFirst & ((int) str[beginPos + noThree]));

		return ((long) (firstByte << noTwentyFour | secondByte << noSixteen
				| thirdByte << noEight | fourthByte))
				& fixthByte;
	}

	/**
	 * convert an integer to a char array.
	 * 
	 * @param val
	 *            an integer
	 * @return a char array
	 */
	private byte[] convertAnInt32(int val) {
		byte[] str = new byte[aByte];
		str[0] = (byte) ((val & fourthByte) >> noTwentyFour);
		str[1] = (byte) ((val & thirdByte) >> noSixteen);
		str[2] = (byte) ((val & secondByte) >> noEight);
		str[noThree] = (byte) ((val & byteFirst));
		return str;
	}

	/**
	 * return the index of a word in entry list.
	 * 
	 * @param word
	 *            the chosen word
	 * @return index of this word
	 */
	public long findIndexForWord(String word) {
		if (!boolIsLoaded)
			return longWordCount;
		long first = 0;
		long last = (int) longWordCount - 1;
		long mid;
		String lwrWord = word.toLowerCase();
		// use binary search
		do {
			mid = (first + last) / 2;
			int cmp = lwrWord.compareTo(((WordEntry) entryList.get((int) mid))
					.getStrLwrWord());
			if (cmp == 0) {
				return mid; // return index if found
			}
			if (cmp > 0) {
				first = mid + 1;
			} else {
				last = mid - 1;
			}
		} while (first <= last);
		// if not found
		/*
		 * if (first < longWordCount) { while (first < longWordCount) { if
		 * (((WordEntry) entryList.get( (int)
		 * first)).getStrLwrWord().compareTo(lwrWord) > 0) { break; } else {
		 * first++; } } }
		 */
		first = -1;
		return first;
	}

	/**
	 * read string from a buffered input stream
	 * 
	 * @param bis
	 *            the buffered input stream
	 * @return read string, null if input stream reach the end
	 * @throws IOException
	 * @throws FileFormatErrorException
	 *             if file format is wrong
	 */
	protected String readString(BufferedInputStream bis) throws IOException,
			FileFormatErrorException {
		bis.mark(readlimit);
		int pos = getNullCharPosition(bis);
		bis.reset();
		if (pos == -1)
			return null;
		byte[] buf = new byte[pos];
		bis.read(buf);
		String result = new String(buf, "UTF8");
		// read null char
		bis.read();

		return result;
	}

	// protected String readString(MappedByteBuffer out) throws IOException,
	// FileFormatErrorException {
	// //bis.mark(readlimit);
	// int pos = getNullCharPosition(out);
	// //bis.reset();
	// if (pos == -1)
	// return null;
	// byte[] buf = new byte[pos];
	// //bis.read(buf);
	// String result = new String(buf, "UTF8");
	// // read null char
	// //bis.read();
	//
	// return result;
	// }

	/**
	 * get null char position
	 * 
	 * @param bis
	 *            buffered input stream
	 * @return null char position
	 * @throws IOException
	 * @throws FileFormatErrorException
	 *             if file format is wrong
	 */
	/**
	 * get null char position
	 * 
	 * @param bis
	 *            buffered input stream
	 * @return null char position
	 * @throws IOException
	 * @throws FileFormatErrorException
	 *             if file format is wrong
	 */

	private int getNullCharPosition(BufferedInputStream bis)
			throws IOException, FileFormatErrorException {
		int length;
		byte[] buf = new byte[256];
		int pos = -1;
		int totalBytes = 0;
		boolean nullCharFound = false;
		while ((length = bis.read(buf)) != -1) {
			totalBytes++;
			for (int i = 0; i < length; i++) {
				pos++;
				if (buf[i] == 0) {
					nullCharFound = true;
					break;
				}
			}
			if (nullCharFound)
				break;
		}

		END_POSITION += totalBytes;

		return pos;
	}

	// private int getNullCharPosition(MappedByteBuffer bis)
	// throws IOException, FileFormatErrorException {
	// int length;
	// byte[] buf = new byte[256];
	// ByteBuffer bb = ByteBuffer.allocate(256);
	// int pos = -1;
	// int totalBytes = 0;
	// boolean nullCharFound = false;
	// int nRead;
	// while ((nRead = bis.re) != -1) {
	// totalBytes++;
	// for (int i = 0; i < length; i++) {
	// pos++;
	// if (buf[i] == 0) {
	// nullCharFound = true;
	// break;
	// }
	// }
	// if (nullCharFound)
	// break;
	// }
	//
	// END_POSITION += totalBytes;
	//
	// return pos;
	// }

	/**
	 * read integer from a buffered input stream, bytes are big endian
	 * 
	 * @param bis
	 *            the buffered input stream
	 * @return read integer, -1 if input stream reach the end
	 * @exception IOException
	 * @exception FileFormatErrorException
	 *                if file format is wrong
	 */
	private int readInt(BufferedInputStream bis) throws IOException,
			FileFormatErrorException {
		byte[] buf = new byte[4];
		int length = bis.read(buf);
		if (length < 4)
			throw new FileFormatErrorException(this.strFileName,
					fileInputStream.getChannel().position());
		int value = 0;

		for (int i = 0; i < 4; i++) {
			value <<= 8;
			value |= (int) buf[i] & 0xFF;
		}
		return value;
	}

	/**
	 * Write to an .idx file.
	 * 
	 * @param fileName
	 *            path to an .idx file
	 * @return true if write success
	 */
	public boolean write(String fileName) {
		try {
			DataOutputStream dt = new DataOutputStream(new FileOutputStream(
					fileName));
			// dt.write(firstBytes,0,Constants.byteFirst_POS_INDEX_FILE);
			WordEntry tempEntry = null;
			for (int i = 0; i < (int) longWordCount; i++) {
				tempEntry = entryList.get(i);
				dt.write(tempEntry.getStrWord().getBytes("UTF8"));
				dt.write('\0');
				dt.write(convertAnInt32((int) tempEntry.getLongOffset()));
				dt.write(convertAnInt32((int) tempEntry.getLongSize()));
			}
			dt.flush();
			dt.close();
			return true;
		} catch (Exception ex) {
			System.out.println("Error: " + ex);
		}
		return false;
	}

	/**
	 * return the result of the write(filename)function.
	 * 
	 * @return true if write success.
	 */
	public boolean write() {
		return write(strFileName);
	}

	/**
	 * add word, offset, size to the entryList.
	 * 
	 * @param word
	 *            the chosen word.
	 * @param offset
	 *            the size of .dict file.
	 * @param size
	 *            the size of word meaning.
	 * @param addPos
	 *            position of added word
	 * @return true if success.
	 */
	public boolean addEntry(String word, long offset, long size, int addPos) {
		WordEntry etr = new WordEntry();
		etr.setStrWord(word);
		etr.setStrLwrWord(word.toLowerCase());
		etr.setLongOffset(offset);
		etr.setLongSize(size);
		if (addPos == -1) {
			addPos = (int) findIndexForWord(etr.getStrLwrWord());
		}
		if (addPos == longWordCount) {
			entryList.add(etr);
			this.longWordCount++;
			longIdxFileSize += (noNine + word.length());
			return true;
		} else if (etr.getStrLwrWord().compareTo(
				((WordEntry) this.entryList.get(addPos)).getStrLwrWord()) != 0) {
			entryList.add(addPos, etr);
			this.longWordCount++;
			longIdxFileSize += (noNine + word.length());
			return true;
		}
		return false;
	}

	/**
	 * remove a word from entryList.
	 * 
	 * @param word
	 *            the chosen word.
	 * @return true if remove success.
	 */
	public boolean removeEntry(String word) {
		String strLwrWord = word.toLowerCase();
		int pos = (int) findIndexForWord(strLwrWord);
		if (pos == longWordCount) {
			return false;
		} else if (strLwrWord.compareTo(((WordEntry) entryList.get(pos))
				.getStrLwrWord()) != 0) {
			return false;
		} else {
			this.longWordCount--;
			longIdxFileSize -= (noNine + word.length());
			entryList.remove(pos);
			return true;
		}
	}
}
